package com.openess.bigsearch.engine.index;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.Collection;

import com.openess.bigsearch.engine.BTree.util.Arrays;
import com.openess.bigsearch.engine.BTree.util.Arrays.Sort;
import com.openess.bigsearch.engine.BTree.util.ByteArrays;
import com.openess.bigsearch.engine.document.Field;
import com.openess.bigsearch.engine.utils.HashMap;
import com.openess.bigsearch.engine.utils.LinkedList;
import com.openess.bigsearch.engine.utils.Map;

/**
 * 这个类主要用于将分散的Extent对象的数据整合到一个集合中，并按词典表-数据表，两个文件的形式写入磁盘.<br>
 * 同时还负责将磁盘中的范围表数据载入内存.<br>
 * 目前是一次查询，载入一个域名对应的所有ExtData[].<br>
 * 并利用二分查找具体DocID对应的Extent数据，以后可以考虑预先把所有范围表加载进内存， 提高查找速度.
 * 
 * @author TuTu
 * 
 */
public class Extent {
	/**
	 * 用于存储范围表中的数据，每个域都对应多个ExtData对象，保存4种数据：1.文档的ID。2.域在文档中的起始位置。3.终止位置 。
	 * 目前域的范围不再是以分词为单位，而是以字符为单位，因此一个域的范围：[start, start+field.length)
	 * 
	 * @author TuTu
	 * 
	 */
	public class ExtData implements Comparable<ExtData> {
		/**
		 * extent对像写入磁盘时，所占的字节数:1.long存储的docID。2.int存储的start。3.int存储的end。
		 */
		public static final int SIZE = 16;

		/**
		 * 写入磁盘前，将数据转换成byte保存于data数组中
		 */
		private byte[] data;
		private long docID;
		private int start;
		/**
		 * 不包括
		 */
		private int end;

		/**
		 * 构造方法，只初始化字节数组data
		 */
		public ExtData() {
			this.data = new byte[SIZE];
		}

		/**
		 * 构造方法只初始化变量，data,docID
		 * 
		 * @param docID
		 */
		public ExtData(long docID) {
			this.data = new byte[SIZE];
			this.docID = docID;
		}

		/**
		 * 构造方法
		 * 
		 * @param docID
		 *            ID是唯一的，因此每个域的数据中，不可能有重复的ID
		 * @param start
		 * @param end
		 */
		public ExtData(long docID, int start, int end) {
			this.data = new byte[SIZE];
			this.docID = docID;
			this.start = start;
			this.end = end;
		}

		@Override
		public int compareTo(ExtData o) {
			// 必须这么写，而不能直接类型转换，因为long类型转int，很可能出问题
			long result = this.docID - o.docID;
			if (result == 0) {
				return 0;
			} else if (result > 0) {
				return 1;
			} else {
				return -1;
			}
		}

		/**
		 * 判断指定的pos至于在[start,end)范围内
		 * 
		 * @param pos
		 * @return
		 */
		public boolean contain(int pos) {
			if (pos >= start && pos < end) {
				return true;
			}
			return false;
		}

		/**
		 * 判断一个pos列表中，是否有属于[start,end)范围的结果。
		 * 如果没有一个pos在范围内，那么返回false，代表这篇文档不符合要求，不显示给用户
		 * 。除了删除文档外，并不对不符合要求的pos进行过滤，因为pos主要用于按域搜索和自动摘要
		 * ，用户按域进行搜索时，不代表他们只希望看到指定域里的摘要，因此域搜索不该对摘要产生影响，因此保留所有pos。
		 * 
		 * @param posList
		 * @return
		 */
		public boolean contain(Integer[] posList) {
			for (int pos : posList)
				if (contain(pos))
					return true;
			return false;
		}

		/**
		 * 得到一个分词在域中的出现频率
		 * 
		 * @param pos
		 *            分词在指定文档中的偏移位置
		 * @return
		 */
		public final int ffrq(Integer[] posList) {
			int ffrq = 0;
			for (int pos : posList) {
				if (pos >= start && pos < end) {
					ffrq++;
				}
			}
			return ffrq;
		}

		public final long getDocID() {
			return docID;
		}

		public final int getEnd() {
			return end;
		}

		public final int getStart() {
			return start;
		}

		/**
		 * 域在指定文档中的长度
		 * 
		 * @return
		 */
		public final int length() {
			return end - start + 1;
		}

		/**
		 * 从磁盘中读取数据，并将所有变量赋值
		 * 
		 * @param offset
		 * @return
		 * @throws IOException
		 */
		public ExtData read(FileChannel cf, long offset) throws IOException {
			ByteBuffer buf = ByteBuffer.wrap(data);
			cf.read(buf, offset);
			docID = ByteArrays.getLong(data, 0);
			start = ByteArrays.getInt(data, 8);
			end = ByteArrays.getInt(data, 12);
			return this;
		}

		@Override
		public String toString() {
			return docID + ":(" + start + "," + end + ")  ";
		}

		/**
		 * 在指定的磁盘偏移位置写入范围表数据
		 * 
		 * @param cf
		 * @param offset
		 * @throws IOException
		 */
		public void write(FileChannel cf, long offset) throws IOException {
			ByteArrays.putLong(docID, data, 0);
			ByteArrays.putInt(start, data, 8);
			ByteArrays.putInt(end, data, 12);
			// 写入磁盘
			ByteBuffer buf = ByteBuffer.wrap(data);
			cf.write(buf, offset);
		}
	}

	/**
	 * 写入Extent对象前，保存的头信息，包括：1.每个分词对应的Extent个数。
	 */
	public static final int EXTHEAD = 4;
	/**
	 * 范围表的词典中保存的头信息，包括：1.有多少种类的域，即有多少个key。2.除了头信息以后，数据总体所占的字节数。
	 */
	public static final int KEYHEAD = 8;
	/**
	 * 关键域，每个数据项的大小
	 */
	public static final int mainSize = 8 + Field.MFSIZE * 4;
	/**
	 * 范围表中的词典表，其当前的偏移位置
	 */
	private int keyOffset;
	/**
	 * 除开头信息，字典表总体所占的字节数
	 */
	private int keySize;
	/**
	 * 将范围表的词典装载在这个集合中，并提供搜索(只读)。<br>
	 * 词典表主要包含如下几种数据：1.域名。2.域在全集中的平均长度。3.这个域对应数据在磁盘的偏移量
	 */
	private HashMap<String, Token<Integer, Long>> extSearch;
	/**
	 * 用于合并/保存范围表的数据，K是域名，V是Extent对象(只写)
	 */
	private HashMap<String, LinkedList<ExtData>> extMap;

	/**
	 * 用于保存关键域的数据(docID, fLengthList[])，并提供搜索，也负责写入磁盘(读/写)
	 */
	private HashMap<Long, Integer[]> mainExtMap;
	/**
	 * 包含范围表中，所有域名的数组
	 */
	private String[] fieldList;

	/**
	 * 用于读写范围的字典，(域名,偏移量)
	 */
	private RandomAccessFile rf1;
	private FileChannel cf1;
	/**
	 * 用于读写范围表的数据，(id:(start,end))
	 */
	private RandomAccessFile rf2;
	private FileChannel cf2;
	/**
	 * 用于读写关键域数据(docID, fLength[])
	 */
	private RandomAccessFile rf3;
	private FileChannel cf3;

	/**
	 * 构造方法
	 * 
	 * @param indexPath
	 * @param mode
	 *            判断ExtData对象是用于读还是用于写，写为"w"，读为"r"，读写一起操作，则为"rw"
	 * @throws IOException
	 */
	public Extent(String indexPath, String mode, int version)
			throws IOException {
		rf1 = new RandomAccessFile(indexPath + "ext_key_" + version, "rw");
		cf1 = rf1.getChannel();
		rf2 = new RandomAccessFile(indexPath + "ext_data_" + version, "rw");
		cf2 = rf2.getChannel();
		rf3 = new RandomAccessFile(indexPath + "ext_main_" + version, "rw");
		cf3 = rf3.getChannel();

		mainExtMap = new HashMap<Long, Integer[]>(500);
		if (mode.contains("w")) {
			extMap = new HashMap<String, LinkedList<ExtData>>();
		}
		if (mode.contains("r")) {
			extSearch = new HashMap<String, Token<Integer, Long>>();
			// 范围表的词典表载入内存
			readExtKey();
			fieldList = new String[extSearch.size()];
			int i = 0;
			for (String key : extSearch.keySet()) {
				fieldList[i++] = key;
			}
			readMain();
		}
	}

	/**
	 * 往集合中添加范围表，待范围表整合完毕，准备写入磁盘
	 * 
	 * @param fileName
	 *            域名
	 * @param docID
	 *            域所在文档的ID
	 * @param start
	 *            其实位置(字符为单位)
	 * @param end
	 *            终止位置，不包括end
	 * @param raw
	 *            原文本
	 * @param textoffset
	 *            原文本存储偏移量
	 */
	public void addExtent(String fileName, long docID, int start, int end) {
		LinkedList<ExtData> value = extMap.get(fileName);
		if (value == null) {
			value = new LinkedList<ExtData>();
			value.add(new ExtData(docID, start, end));
			extMap.put(fileName, value);
		} else
			value.add(new ExtData(docID, start, end));
	}

	/**
	 * 将(key,avfLength,offset)数据对装载到HashMap集合中，提供搜索服务，这个集合应该一直保存在内存中
	 * 
	 * @param key
	 * @param avfLength
	 * @param offset
	 */
	private void addKeys(String key, int avfLength, long offset) {
		extSearch.put(key, new Token<Integer, Long>(avfLength, offset));
	}

	/**
	 * 关键域的数据单独添加，准备写入磁盘
	 * 
	 * @param docID
	 * @param fLength
	 */
	public void addMainExt(Long docID, Integer[] fLength) {
		mainExtMap.put(docID, fLength);
	}

	/**
	 * 计算集合中一个域的平均长度
	 * 
	 * @param list
	 *            一个list代表一个域名对应的所有范围
	 * @return
	 */
	private int avfLength(ExtData[] list) {
		int result = 0;
		for (ExtData e : list) {
			result += e.end - e.start;
		}
		return result / list.length;
	}

	/**
	 * 关闭流，释放资源
	 * 
	 * @throws IOException
	 */
	public void close() throws IOException {
		cf1.close();
		cf2.close();
		rf1.close();
		rf2.close();
	}

	/**
	 * 保存的域的种类的数量，可能出现同域名的情况
	 * 
	 * @return
	 */
	public int fieldSize() {
		return extSearch.size();
	}

	/**
	 * 拿到所有的域名
	 * 
	 * @return
	 */
	public final String[] getFieldList() {
		return fieldList;
	}

	public HashMap<Long, Integer[]> getMainExtMap() {
		return mainExtMap;
	}

	/**
	 * 将范围表的词典，从磁盘中读取出来，放入内存散列表中，以备搜索之用
	 * 
	 * @return
	 * @throws IOException
	 */
	private HashMap<String, Token<Integer, Long>> readExtKey()
			throws IOException {
		int desPos = 0;
		// 关键字个数
		int size = readExtKeyHead();
		// 从磁盘中将所有字典表数据读取出来
		byte[] data = new byte[keySize];
		ByteBuffer buf = ByteBuffer.wrap(data);
		cf1.read(buf, KEYHEAD);
		for (int i = 0; i < size; i++) {
			int length = ByteArrays.getShort(data, desPos);
			desPos += 2;
			String key = ByteArrays.getString(data, desPos, length);
			desPos += length * 2;
			int avfLength = ByteArrays.getInt(data, desPos);
			desPos += 4;
			long offset = ByteArrays.getLong(data, desPos);
			desPos += 8;
			addKeys(key, avfLength, offset);
		}
		return extSearch;
	}

	/**
	 * 读取范围表中的词典表的头信息，即key的个数
	 * 
	 * @return 词典表中的key个数
	 * @throws IOException
	 */
	private int readExtKeyHead() throws IOException {
		byte[] head = new byte[KEYHEAD];
		ByteBuffer buf = ByteBuffer.wrap(head);
		cf1.read(buf, 0);
		keySize = ByteArrays.getInt(head, 4);
		return ByteArrays.getInt(head, 0);
	}

	/**
	 * 拿到域名对应的ExtData对象数组，因此是一次性加载一个域名对应的所有数据
	 * 
	 * @param field
	 * @return
	 * @throws IOException
	 */
	public ExtData[] readExtValue(String field) throws IOException {
		Token<Integer, Long> t = extSearch.get(field);
		long offset = t.getValue();
		// 拿到头信息
		int length = readExtValueHead(offset);
		ExtData[] results = new ExtData[length];
		offset += EXTHEAD;
		for (int i = 0; i < length; i++) {
			results[i] = new ExtData().read(cf2, offset);
			offset += ExtData.SIZE;
		}
		return results;
	}

	/**
	 * 读取每个域保存的头信息，包括：1.每个分词对应的Extent个数
	 * 
	 * @param offset
	 * @return 返回分词对应的Extent个数
	 * @throws IOException
	 */
	private int readExtValueHead(long offset) throws IOException {
		byte[] head = new byte[EXTHEAD];
		ByteBuffer buf = ByteBuffer.wrap(head);
		cf2.read(buf, offset);
		return ByteArrays.getInt(head, 0);
	}

	/**
	 * 从磁盘中读取关键字数据
	 * 
	 * @throws IOException
	 */
	private void readMain() throws IOException {
		byte[] head = new byte[4];
		ByteBuffer buf = ByteBuffer.wrap(head);
		cf3.read(buf);
		int size = ByteArrays.getInt(head, 0);
		byte[] data = new byte[mainSize];
		for (int i = 0; i < size; i++) {
			buf = ByteBuffer.wrap(data);
			cf3.read(buf);
			long docID = ByteArrays.getLong(data, 0);
			Integer[] value = new Integer[Field.MFSIZE];
			for (int j = 0; j < Field.MFSIZE; j++) {
				value[j] = ByteArrays.getInt(data, 8 + j * 4);
			}
			mainExtMap.put(docID, value);
		}
	}

	/**
	 * 根据域名，从词典表中取出域的平均长度
	 * 
	 * @param key
	 * @return
	 */
	public int searchAvfLength(String key) {
		Token<Integer, Long> t = extSearch.get(key);
		return t.getKey();
	}

	/**
	 * 根据给定的域名和docID，拿到范围表中指定的Extent，如果不存在，返回空。目前的查找暂时不支持多个同名域，应该让二分查找可以返回多个结果。<br>
	 * 现在的这个搜索效率极低，最好避免使用，为了查找指定文档-指定域的范围表，需要拿到域中所有的数据。
	 * 
	 * @param fieldname
	 * @param docID
	 * @return
	 * @throws IOException
	 */
	public ExtData searchExt(String fieldname, long docID) throws IOException {
		// 读取出来的ExtData对象并没有放入散列表中提供搜索，而是直接使用二分查找对数组进行查找，以后改进可以考虑使用散列表把所有的域都预先装载进内存
		ExtData[] results = readExtValue(fieldname);
		ExtData query = new ExtData(docID);
		int i = Arrays.binarySearch(results, query);
		if (i < 0) {
			return null;
		}
		return results[i];
	}

	/**
	 * 按域拿到所有的返回范围表
	 * 
	 * @param fieldname
	 * @return
	 * @throws IOException
	 */
	public ExtData[] searchExtList(String fieldname) throws IOException {
		return readExtValue(fieldname);
	}

	/**
	 * 拿到指定文档的所有关键域数据
	 * 
	 * @param docID
	 * @return
	 */
	public Integer[] searchMain(Long docID) {
		return mainExtMap.get(docID);
	}

	/**
	 * 将Extent对象中每个fieldName对应的列表按DocID进行排序
	 * 
	 * @param token
	 * @return
	 */
	private ExtData[] sortExtent(Collection<ExtData> list) {
		ExtData[] array = list.toArray(new ExtData[0]);
		Arrays.sort(array, Sort.HEAP);
		return array;
	}

	/**
	 * 将范围表写入磁盘，范围表分三部分写入：1.词典表(域名，域平均长度，偏移量)采用磁盘顺序存储。2.真实数据。3.原文本
	 * 
	 * @throws IOException
	 */
	public void writeExtent() throws IOException {
		try {
			long offset = 0;
			keyOffset += KEYHEAD;
			for (Map.Entry<String, LinkedList<ExtData>> e : extMap.entrySet()) {
				// 按照docID进行排序
				ExtData[] value = sortExtent(e.getValue());
				int avfLength = avfLength(value);
				writeExtKey(e.getKey(), avfLength, offset);
				offset = writeExtValue(value, offset);
			}
			// 顺序不能颠倒，因为头信息中要保存keySize变量的值
			keySize = keyOffset - KEYHEAD;
			writeExtKeyHead();
			extMap = null;
			writeMain();
		} finally {
			cf1.close();
			cf2.close();
			rf1.close();
			rf2.close();
		}
	}

	/**
	 * 写入词典表，往磁盘中写入域名和其数据对应的起始偏移量
	 * 
	 * @param key
	 *            域名
	 * @param avfLength
	 *            指定域在集合中的平均长度
	 * @param offset
	 *            关键字对应的数据所在的偏移量，而不是关键字本身的偏移量
	 * @throws IOException
	 */
	private void writeExtKey(String key, int avfLength, long offset)
			throws IOException {
		// 前两个字节是关键字的长度，用的short类型进行存储
		int size = 2 + key.length() * 2;
		// 后面保存的12个字节是域名对应的域平均长度和数据的偏移量
		byte[] dest = new byte[size + 4 + 8];
		ByteArrays.putString(key, dest, 0);
		ByteArrays.putInt(avfLength, dest, size);
		ByteArrays.putLong(offset, dest, size + 4);
		ByteBuffer buf = ByteBuffer.wrap(dest);
		cf1.write(buf, keyOffset);
		keyOffset += (size + 12);
	}

	/**
	 * 写入范围表中的词典表中的头信息，头信息包括：1.有多少种类的域，即有多少个key。2.数据总体所占的字节数(不包括头信息)
	 * 
	 * @throws IOException
	 */
	private void writeExtKeyHead() throws IOException {
		byte[] head = new byte[KEYHEAD];
		ByteArrays.putInt(extMap.size(), head, 0);
		ByteArrays.putInt(keySize, head, 4);
		ByteBuffer buf = ByteBuffer.wrap(head);
		cf1.write(buf, 0);
	}

	/**
	 * 往磁盘中写入域名对应的数据
	 * 
	 * @param list
	 * @param offset
	 * @return 返回下一次应该从哪里开始继续写
	 * @throws IOException
	 */
	private long writeExtValue(ExtData[] list, long offset) throws IOException {
		writeExtValueHead(list, offset);
		offset += EXTHEAD;
		for (ExtData ext : list) {
			ext.write(cf2, offset);
			offset += ExtData.SIZE;
		}
		return offset;
	}

	/**
	 * 写入每个域的头信息，包括：1.每个分词对应的Extent个数
	 * 
	 * @param list
	 * @param offset
	 * @throws IOException
	 */
	private void writeExtValueHead(ExtData[] list, long offset)
			throws IOException {
		int length = list.length;
		// 写入头数据，头数据包括：1.list的长度。
		byte[] head = new byte[EXTHEAD];
		ByteArrays.putInt(length, head, 0);
		ByteBuffer buf = ByteBuffer.wrap(head);
		cf2.write(buf, offset);
	}

	/**
	 * 往磁盘中写入关键域数据
	 * 
	 * @throws IOException
	 */
	private void writeMain() throws IOException {
		try {
			// 头数据，保存总共有多少个文档
			byte[] head = new byte[4];
			ByteArrays.putInt(mainExtMap.size(), head, 0);
			ByteBuffer buf = ByteBuffer.wrap(head);
			cf3.write(buf);
			for (Map.Entry<Long, Integer[]> e : mainExtMap.entrySet()) {
				byte[] data = new byte[mainSize];
				long docID = e.getKey();
				Integer[] value = e.getValue();
				ByteArrays.putLong(docID, data, 0);
				for (int i = 0; i < value.length; i++) {
					Integer temp = value[i];
					if (temp == null)
						temp = 0;
					ByteArrays.putInt(temp, data, 8 + i * 4);
				}
				buf = ByteBuffer.wrap(data);
				cf3.write(buf);
			}
		} finally {
			cf3.close();
			rf3.close();
		}
	}
}
